Skip to content

feat!: enrich PVT solution with IFB reference bands, units, and course over ground#50

Merged
giove-a merged 1 commit into
masterfrom
sc/enrich-pvt-solution
Jul 7, 2026
Merged

feat!: enrich PVT solution with IFB reference bands, units, and course over ground#50
giove-a merged 1 commit into
masterfrom
sc/enrich-pvt-solution

Conversation

@giove-a

@giove-a giove-a commented Jul 7, 2026

Copy link
Copy Markdown

feat!: enrich PVT solution with IFB reference bands, units, and course over ground

Summary

Enriches PVTSolution with an explicit inter-frequency-bias representation, a
course-over-ground field, and Unitful units on its metre-valued fields, and
tightens the warm-start seeding of the inter-frequency biases. Also fixes a
code-phase double-count in calc_uncorrected_time for jointly tracked satellites.

This is a breaking release (4.0.0): the PVTSolution field layout and several
field types change.

What changed

Inter-frequency biases carry their reference band (InterFrequencyBias)

inter_frequency_biases is now Dict{Symbol, InterFrequencyBias} instead of
Dict{Symbol, Float64}. Each InterFrequencyBias bundles:

  • value::typeof(1.0m) — the bias, in metres, relative to reference;
  • reference::Symbol — the band its bias is measured against (the anchor whose
    delay is folded into the clock biases).

The reference band is chosen per coverage component and threaded from
band_ifb_layoutdecide_bias_layoutcalc_pvt, so each reported bias's
anchor is explicit rather than implicit. A disconnected constellation can in
principle report distinct references per band.

Course over ground

Adds course_over_ground::typeof(1.0°): the azimuth of the velocity vector in the
local ENU frame at position, clockwise from true North, wrapped to [0, 360)°
(the GNSS COG convention). It is the velocity-derived direction of motion, not
vehicle heading — a single-antenna receiver cannot observe heading. when the
horizontal velocity is zero. Computed by the new calc_course_over_ground helper.

Units on metre-valued fields

time_correction, the inter_system_biases values, InterFrequencyBias.value, and
SatInfo.residual are now typeof(1.0m) Unitful quantities (joining the
degree-valued course_over_ground). position/velocity stay plain ECEF (Geodesy
interop) and relative_clock_drift stays dimensionless. The least-squares solver
still runs on plain Float64; units are attached only at the output boundary and
stripped at the warm-start seed, so there is no runtime cost (a Quantity{Float64}
is bit-identical to a Float64).

Reference-aware IFB warm start

Each IFB column is seeded from the previous solution only when that band's reference
band is unchanged. On a reference change the stored value refers to a different
anchor, so the column is seeded at 0 instead. The IFB enters the design linearly,
so this affects convergence, not the converged estimate.

Fix: calc_uncorrected_time code-phase double-count

For jointly tracked satellites, reduce the shared code phase modulo one data symbol
so the whole-symbol part already counted by the bit term is not added twice (a no-op
for single-signal sats). E5a/L2C test fixtures updated to build realistic
measurements (whole data symbols in the bit count, residual chips in the code phase).

Breaking changes / migration

  • PVTSolution gains course_over_ground, changing the positional-constructor
    arity/order and the field layout — update positional construction and
    reflection-based code. Keyword construction is unaffected.
  • time_correction, inter_system_biases values, and SatInfo.residual are now
    Unitful typeof(1.0m) quantities, not bare Float64 metres. Code doing
    arithmetic on them must handle units, e.g. ustrip(u"m", pvt.time_correction).
  • inter_frequency_biases is now Dict{Symbol, InterFrequencyBias}. Read
    pvt.inter_frequency_biases[:L5].value (a typeof(1.0m)) and .reference
    (the anchor band) instead of a bare Float64.

Testing

  • Full test suite passes, including the inter-frequency-bias set (unit assertions
    updated to compare against Unitful quantities) and the new
    calc_course_over_ground tests.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.30%. Comparing base (e474a85) to head (b17cf63).

Additional details and impacted files
@@            Coverage Diff             @@
##           master      #50      +/-   ##
==========================================
+ Coverage   98.27%   98.30%   +0.02%     
==========================================
  Files           7        7              
  Lines         464      472       +8     
==========================================
+ Hits          456      464       +8     
  Misses          8        8              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results (Julia v1)

Time benchmarks
master b17cf63... master / b17cf63...
calc_pvt/GPSL1/4sats/cold 0.0429 ± 0.0064 ms 0.0437 ± 0.0065 ms 0.981 ± 0.21
calc_pvt/GPSL1/4sats/warm 0.0376 ± 0.0064 ms 0.0388 ± 0.0066 ms 0.971 ± 0.23
calc_pvt/GPSL1/9sats/cold 0.063 ± 0.0014 ms 0.0635 ± 0.0014 ms 0.993 ± 0.031
calc_pvt/GPSL1/9sats/warm 0.0395 ± 0.0057 ms 0.0396 ± 0.00082 ms 0.996 ± 0.15
calc_pvt/GalileoE1B/4sats/cold 0.0381 ± 0.0065 ms 0.0382 ± 0.007 ms 0.995 ± 0.25
calc_pvt/GalileoE1B/4sats/warm 18.4 ± 3.2 μs 18.6 ± 2.5 μs 0.985 ± 0.21
calc_pvt/GalileoE1B/5sats/cold 0.0409 ± 0.0061 ms 0.041 ± 0.007 ms 0.997 ± 0.23
calc_pvt/GalileoE1B/5sats/warm 21.4 ± 4.1 μs 21.8 ± 4.3 μs 0.982 ± 0.27
time_to_load 2.39 ± 0.018 s 2.39 ± 0.015 s 0.997 ± 0.0099
Memory benchmarks
master b17cf63... master / b17cf63...
calc_pvt/GPSL1/4sats/cold 0.551 k allocs: 25.1 kB 0.552 k allocs: 25.2 kB 0.999
calc_pvt/GPSL1/4sats/warm 0.492 k allocs: 21.5 kB 0.493 k allocs: 21.5 kB 0.999
calc_pvt/GPSL1/9sats/cold 0.779 k allocs: 31.4 kB 0.78 k allocs: 31.5 kB 0.999
calc_pvt/GPSL1/9sats/warm 0.621 k allocs: 21.6 kB 0.622 k allocs: 21.7 kB 0.999
calc_pvt/GalileoE1B/4sats/cold 0.54 k allocs: 24.5 kB 0.541 k allocs: 24.5 kB 0.999
calc_pvt/GalileoE1B/4sats/warm 0.371 k allocs: 14.6 kB 0.372 k allocs: 14.7 kB 0.998
calc_pvt/GalileoE1B/5sats/cold 0.579 k allocs: 25.2 kB 0.58 k allocs: 25.2 kB 0.999
calc_pvt/GalileoE1B/5sats/warm 0.421 k allocs: 15.9 kB 0.422 k allocs: 15.9 kB 0.998
time_to_load 0.149 k allocs: 11.2 kB 0.149 k allocs: 11.2 kB 1

…e over ground

- Replace the raw `inter_frequency_biases::Dict{Symbol,Float64}` with
  `Dict{Symbol,InterFrequencyBias}`, where each `InterFrequencyBias` bundles the
  bias `value` with the `reference` band it is measured against — the anchor whose
  delay is folded into the clock biases. `band_ifb_layout`/`decide_bias_layout` now
  thread the per-coverage-component reference band through to `calc_pvt`, so each
  bias's anchor is explicit rather than implicit, and a disconnected constellation
  can report distinct references per band.

- Add `course_over_ground::typeof(1.0°)` to `PVTSolution`: the azimuth of the
  velocity vector in the local ENU frame, degrees clockwise from true North in
  `[0, 360)°`, via the new `calc_course_over_ground` helper. This is the
  velocity-derived direction of motion (COG), not vehicle heading.

- Attach Unitful units to the metre-valued fields of `PVTSolution`/`SatInfo`:
  `time_correction`, the `inter_system_biases` values, `InterFrequencyBias.value`
  and `SatInfo.residual` are now `typeof(1.0m)`, alongside the degree-valued
  `course_over_ground`. The least-squares solver still runs on plain `Float64`;
  units are attached only at the output boundary and stripped at the warm-start
  seed, so there is no runtime cost.

- Warm-start each IFB column from the previous solution only when that band's
  reference band is unchanged; a reference change makes the stored value refer to a
  different anchor, so the column is seeded at 0 instead. (The IFB enters the design
  linearly, so this affects convergence, not the converged estimate.)

- Fix `calc_uncorrected_time` double-counting the code phase for jointly tracked
  satellites: reduce the shared code phase modulo one data symbol so the
  whole-symbol part already counted by the bit term is not added twice (a no-op for
  single-signal sats). Update the E5a/L2C test fixtures to build realistic
  measurements (whole data symbols in the bit count, residual chips in the code
  phase) instead of packing the whole sub-TOW interval into the code phase.

BREAKING CHANGE: `PVTSolution` gains a `course_over_ground` field, so the exported
struct's positional-constructor arity/order and field layout change. Several fields
are now Unitful quantities rather than bare `Float64` metres: `time_correction`, the
`inter_system_biases` values and `SatInfo.residual` are `typeof(1.0m)`. And
`inter_frequency_biases` is now `Dict{Symbol,InterFrequencyBias}` — read `.value`
(a `typeof(1.0m)`) and `.reference` (the anchor band) instead of a bare `Float64`.
Consumers doing arithmetic must handle the units (e.g. `ustrip(u"m", x)`), and
positional construction and reflection-based code must be updated. Reading via
keyword construction and the accessor fields is otherwise unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@giove-a
giove-a force-pushed the sc/enrich-pvt-solution branch from 57182f9 to b17cf63 Compare July 7, 2026 09:51
@giove-a
giove-a merged commit 6a528ce into master Jul 7, 2026
8 checks passed
@giove-a
giove-a deleted the sc/enrich-pvt-solution branch July 7, 2026 09:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants